/*
 * Copyright Hundsun Technologies Inc. All Rights Reserved.
 */

package keystore

import (
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"sync"

	"hundsun.com/hsl/hschain/common/config"
	"hundsun.com/hsl/hschain/common/crypto"
	"hundsun.com/hsl/hschain/common/types"
	"hundsun.com/hsl/hschain/common/util"
	"hundsun.com/hsl/hschain/protos"
)

const (
	configKeyKeystoreFile   = "chain.keystore.file"
	configKeyKeystorePasswd = "chain.keystore.passwd"
)

var (
	errInvalidPasswd = errors.New("password of keystore is invalid")
)

// KeyStore 用于存储加密的私钥
type KeyStore interface {
	//Load 从指定路径加载keystore
	Load(path string, password string) error
	//GetAccount 从keystore中获取一个账户，找不到返回nil
	GetAccount(address protos.Address) Account

	//GetAllAccounts 获取所有账户
	GetAllAccounts() []Account
}

// Account 代表一个公私钥对及其派生出来的地址
type Account interface {
	//GetAddress 获取地址
	GetAddress() protos.Address
	// GetKeyPair 获取公私钥对
	GetKeyPair() crypto.KeyPair
}

var keyStore KeyStore
var once sync.Once

// GetKeyStore 默认配置文件配置的keystore
func GetKeyStore() KeyStore {
	var err error
	once.Do(func() {
		keyStoreFile := config.GetStringConfigValue(configKeyKeystoreFile, "")
		if keyStoreFile == "" {
			err = errors.New("config '" + configKeyKeystoreFile + "' not found")
			return
		}
		keyStorePasswd := config.GetStringConfigValue(configKeyKeystorePasswd, "")
		if keyStorePasswd == "" {
			err = errors.New("config '" + configKeyKeystorePasswd + "' not found")
			return
		}
		keyStore, err = LoadKeyStore(keyStoreFile, keyStorePasswd)
		if err != nil {
			panic(err)
		}
	})

	return keyStore
}

//LoadKeyStore load key store
func LoadKeyStore(keyStoreFile string, keyStorePasswd string) (KeyStore, error) {
	ks := &fileKeyStore{}
	err := ks.Load(keyStoreFile, keyStorePasswd)
	if err != nil {
		return nil, err
	}
	return ks, err
}

type fileKeyStore struct {
	path        string
	password    string
	accounts    []Account
	accountsMap map[protos.Address]*accountImpl
}

//Load load key keystore file
func (ks *fileKeyStore) Load(path string, password string) error {
	ks.path = path
	ks.password = password
	keystoreBytes, err := util.ReadFile(ks.path)
	if err != nil {
		return err
	}
	var encryptedAccountInfo types.EncryptedAccountInfo
	err = json.Unmarshal(keystoreBytes, &encryptedAccountInfo)
	if err != nil {
		return err
	}
	if len(encryptedAccountInfo.AccountList) == 0 {
		return errors.New("no accountsMap in keystore " + ks.path)
	}
	ks.accounts = make([]Account, len(encryptedAccountInfo.AccountList))
	ks.accountsMap = make(map[protos.Address]*accountImpl, len(encryptedAccountInfo.AccountList))
	for i, acct := range encryptedAccountInfo.AccountList {
		addr, privKey, err := ks.decryptAccount(acct, ks.password)
		if err != nil {
			panic(fmt.Errorf("failed to decrypt account '%s' in keystore file:%s.Please check crypto config in genesis.block and keystore password", acct.Address, err.Error()))
		}
		acct := &accountImpl{
			Address: addr,
			KeyPair: privKey,
		}
		ks.accountsMap[addr] = acct
		ks.accounts[i] = acct
	}
	return nil
}

//GetAccount get account from keystore file
func (ks *fileKeyStore) GetAccount(address protos.Address) Account {
	if len(ks.accountsMap) > 0 {
		if item, ok := ks.accountsMap[address]; ok {
			return item
		}
	}
	return nil
}

//GetAllAccounts get all accounts from keystore file
func (ks *fileKeyStore) GetAllAccounts() []Account {
	return ks.accounts
}

func (ks *fileKeyStore) decryptAccount(acct types.EncryptedAccount, password string) (address protos.Address, privKey crypto.PrivateKey, err error) {
	var cipherBytes []byte
	cipherBytes, err = hex.DecodeString(acct.Ciphertext)
	if err != nil {
		return
	}

	hash, _ := crypto.Hash([]byte(password))
	key, _ := crypto.ParserSymmetricKey(hash)
	decrypted, err := crypto.Decrypt(key, cipherBytes)
	if err != nil {
		return
	}

	privKey, err = crypto.ParserPrivateKey(decrypted)
	if err != nil {
		return
	}
	pubKeyBytes := privKey.PublicKey().Bytes()
	address, err = protos.ParseAddressFromPublicKeyBytes(pubKeyBytes)
	if err != nil {
		return
	}
	if string(address) != acct.Address {
		err = errInvalidPasswd
		return
	}
	return
}

type accountImpl struct {
	Address protos.Address
	KeyPair crypto.KeyPair
}

//GetAddress get address from an account
func (a *accountImpl) GetAddress() protos.Address {
	return a.Address
}

//GetKeyPair get key pair from an account
func (a accountImpl) GetKeyPair() crypto.KeyPair {
	return a.KeyPair
}
